/*
 *  Copyright 2010 Applied Card Technologies Ltd
 *  What : Class ComponentAnnotationProcessor
 *  Who  : Farouk Alhassan
 *  When : Friday, February 19, 2010
 */
package com.farent.solutions.common.discovery;

import com.farent.solutions.common.discovery.ClassResolver.ClassSelectionStrategy;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * Singleton class for caching annotations and classes that posses them for
 * referencing at runtime.
 * 
 * @author farouka
 */
public abstract class AbstractClassResolverProcessor
{
    
    private static Set<Class> classesToDiscover = new HashSet<Class>();

    private static Map<Class, List<Class>> classToInstanceMap = new Hashtable<Class, List<Class>> ();

    /**
     * Keep a cache of only 12 annotations at any instantaneous time T!
     */
    private static final int CACHE_SIZE = 12;
    
    /**
     * A Simple Least reusable cache that maintains only 12 entries. This is to reduce the lookup
     * time for looking up the classes for an annotation.
     *
     * usage: if a request is made for the classes of an annotation then the cache should be checked to
     * see if it has been looked.(a cache-hit) else cache-it once found. This will reduce the lookup time hopefully
     *
     * Also, the Least used annotation will drop off when the limit is reached
     */
    private LRUCache<Class, List<Class>>  classesCache = new LRUCache<Class, List<Class>> ( CACHE_SIZE );
    
    public abstract ClassSelectionStrategy createClassSelectionStrategy( Class clazz );
    
//    public abstract List<Class> getClassList( String pkgName );
    
    public final void loadClasses ( String pkgName )
    {

        List classList = getClassList( pkgName );
        for ( int i = 0; i < classList.size (); i++ )
        {
            //Get each class
            Class topClass = ( Class ) classList.get ( i );

            //Add the annotations on this class to the classAnnotationsMap

            //Find Annotations on class
            List<Class> annotsOnClass =  _annosPresent( topClass );

            //Store Class with List of annotations in map
            if( annotsOnClass != null && annotsOnClass.size () > 0){
                classToInstanceMap.put ( topClass, annotsOnClass );
            }

            //get nested inner classes
            Class[] innerClasses = topClass.getDeclaredClasses ();
            for ( int j = 0; j < innerClasses.length; j++ )
            {
                Class innerClass = innerClasses[j];
                //Find Annotations on class
                List<Class> annotsOnInnerClass =  _annosPresent( topClass );

                //Store Class with List of annotations in map
                if( annotsOnInnerClass != null && annotsOnInnerClass.size () > 0){
                    //If annotations have already been added to this class
                    if( classToInstanceMap.get ( innerClass ) != null ){
                        classToInstanceMap.get ( innerClass ).addAll ( annotsOnInnerClass );
                    } else {
                        classToInstanceMap.put ( innerClass, annotsOnInnerClass );
                    }
                }
                //Get all Methods on class
                Method[] innerClassMethods = innerClass.getMethods ();
                for ( int k = 0; k < innerClassMethods.length; k++ )
                {
                    Method innerMethod = innerClassMethods[k];
                    //Find Annotations on Methods
                    List<Class> annotsOnInnerMethod =  _annosPresentOnMethod( innerMethod );
                    if( annotsOnInnerMethod != null && annotsOnInnerMethod.size () > 0){
                        //If annotations have already been added to this class
                        if( classToInstanceMap.get ( innerClass ) != null ){
                            classToInstanceMap.get ( innerClass ).addAll ( annotsOnInnerMethod );
                        } else {
                            classToInstanceMap.put ( innerClass, annotsOnInnerMethod );
                        }
                    }
                }
            }

            //Get all Methods on class
            Method[] classMethods = topClass.getMethods ();
            for ( int j = 0; j < classMethods.length; j++ )
            {
                Method method = classMethods[j];
                //Find Annotations on Methods
                List<Class> annotsOnMethod =  _annosPresentOnMethod( method );
                if( annotsOnMethod != null && annotsOnMethod.size () > 0){
                    //If annotations have already been added to this class
                    if( classToInstanceMap.get ( topClass ) != null ){
                        classToInstanceMap.get ( topClass ).addAll ( annotsOnMethod );
                    } else {
                        classToInstanceMap.put ( topClass, annotsOnMethod );
                    }
                }
            }

            //Get all Feilds on class
            Field[] classFeilds = topClass.getDeclaredFields ();
            for ( int j = 0; j < classFeilds.length; j++ )
            {
                Field field = classFeilds[j];
                //Find Annotations on Feild
                List<Class> annotsOnMethod =  _annosPresentOnFeild( field );
                if( annotsOnMethod != null && annotsOnMethod.size () > 0){
                    //If annotations have already been added to this class
                    if( classToInstanceMap.get ( topClass ) != null ){
                        classToInstanceMap.get ( topClass ).addAll ( annotsOnMethod );
                    } else {
                        classToInstanceMap.put ( topClass, annotsOnMethod );
                    }
                }
            }
        }
    }
    
    public List<Class> getClassList( String pkgName ) {
        List classList = new ArrayList ();

        List<Class> all =  getAllAnnotations();
        ClassSelectionStrategy selectionStrategy = null;
        for ( int i = 0; i < all.size(); i ++ )
        {
            Class clazz = all.get( i );
            selectionStrategy = createClassSelectionStrategy( clazz );
            classList.addAll ( getClassPropertyList ( selectionStrategy, pkgName ) );
        }
        return classList;
    }
    
    public List<Class> getClasssesImplementing( Class clzz ) {
        List classList = new ArrayList ();

        ClassSelectionStrategy selectionStrategy = new IsAssignableFromSelectionStrategy(clzz);
        classList.addAll ( getClassPropertyList ( selectionStrategy, getRootPackage() ) );
        return classList;
    }

    /**
     * This is an internal inspection method for looking up annotations on a class
     * Must not be used on out of here
     *
     * @param clz
     * @return
     */
    private List _annosPresent( Class clz ){
        List<Class> classAnnos = new ArrayList<Class> ();

        for ( Iterator<Class> it = classesToDiscover.iterator (); it.hasNext (); )
        {
            Class anno = it.next ();
            if( clz.isAnnotationPresent ( anno )){
                classAnnos.add ( anno );
            }
        }
        return classAnnos;
    }


    private List<Class> _annosPresentOnMethod ( Method method )
    {
        List<Class> annosOnMethod = new ArrayList<Class> ();
        for ( Iterator<Class> it = classesToDiscover.iterator (); it.hasNext (); )
        {
            Class anno = it.next ();
            if ( method.isAnnotationPresent ( anno ) )
            {
                annosOnMethod.add ( anno );
            }
        }

        return annosOnMethod;
    }

    private List<Class> _annosPresentOnFeild ( Field field )
    {
        List<Class> annosOnField = new ArrayList<Class> ();
        for ( Iterator<Class> it = classesToDiscover.iterator (); it.hasNext (); )
        {
            Class anno = it.next ();
            if ( field.isAnnotationPresent ( anno ) )
            {
                annosOnField.add ( anno );
            }
        }

        return annosOnField;
    }

    /**
     * Attempts to find classes by auto-discovery in packages specified
     * by pkgName
     *
     * @param selectionStrategy the strategy to use
     * @return a List of classes found
     */
    protected static <T> List<Class<? extends T>> getClassPropertyList (ClassSelectionStrategy selectionStrategy,
            String pkgName )
    {
        ClassResolver<T> resolver = new ClassResolver<T> ();
        String[] packages = ClassResolver.standardSplit ( pkgName );
        resolver.discover ( selectionStrategy, packages );
        Set<Class<? extends T>> classes = resolver.getClasses ();
        removeAbstractClasses ( classes );
        return new ArrayList<Class<? extends T>> ( classes );
    }

    /**
     * Removes any classes from the collection that are abstract or interfaces.
     * 
     */
    protected static <T> void removeAbstractClasses ( Collection<Class<? extends T>> classes )
    {
        Iterator<Class<? extends T>> iterator = classes.iterator ();
        while ( iterator.hasNext () )
        {
            Class<? extends T> clazz = iterator.next ();
            if ( clazz.isInterface () )
            {
                iterator.remove ();
            } else if ( ( clazz.getModifiers () & Modifier.ABSTRACT ) == Modifier.ABSTRACT )
            {
                iterator.remove ();
            }
        }
    }

    /**
     * Central method for resistering annotations
     * This implemenation uses a set so no annotation can be registered
     * twice or null values dont slip in
     * 
     * @param cl annotated class
     * @return whether the addition was successful {@link java.util.Set}
     */
    public boolean addAnnotation ( Class cl )
    {
        return classesToDiscover.add ( cl );
    }

    
    public abstract String getRootPackage();

    /**
     * Initialization method for adding registered annotation classes
     *
     * a local cache of them for future use
     */
    public void initialize(){
        List<Class> annoList = getAllAnnotations();
        for ( int i = 0; i < annoList.size (); i++ )
        {
            Class class1 = annoList.get ( i );
            addAnnotation ( class1 );
        }
        loadClasses( getRootPackage() );
    }

    public Set<Class> getAnnotations ()
    {
        return classesToDiscover;
    }

    /**
     * Retreive all the classes that have
     * this annotation on any of their members
     * 
     * @param annotation the annotation to look for
     * @return classes with this annotation on members
     */
    public List<Class> getClassesWithAnnotation ( Class annotation )
    {
        //Check the cache first. 
        List<Class> foundClasses = classesCache.get ( annotation );
        if ( foundClasses != null )
        {
            return foundClasses;
        } else
        {
            foundClasses = new ArrayList<Class> ();
            Set keyset = classToInstanceMap.keySet ();
            for ( Iterator it = keyset.iterator (); it.hasNext (); )
            {
                Class classWithAnno = ( Class ) it.next ();
                List annos = classToInstanceMap.get ( classWithAnno );
                for ( int i = 0; i < annos.size (); i++ )
                {
                    Class foundAnno = ( Class ) annos.get ( i );
                    if ( foundAnno.getName ().equals ( annotation.getName () ) )
                    {
                        foundClasses.add ( classWithAnno );
                    }
                }
            }
            
            //just before you go, store it in the cache.
            classesCache.put ( annotation, foundClasses );
        }
        return foundClasses;
    }

    /**
     * Method for retreiving all the annotations  for the specified
     * class
     * @param clazz class for whose annotations are searched for
     * @return
     */
    public List<Class> getAnnotationsForClass ( Class clazz )
    {
        List clz = classToInstanceMap.get ( clazz );
        return clz;
    }

    public abstract List<Class> getAllAnnotations();

    public class LRUCache<K, V>
    {

        private static final float hashTableLoadFactor = 0.75f;
        private LinkedHashMap<K, V> map;
        private int cacheSize;

        /**
         * Creates a new LRU cache.
         * @param cacheSize the maximum number of entries that will be kept in this cache.
         */
        public LRUCache ( int cacheSize )
        {
            this.cacheSize = cacheSize;
            int hashTableCapacity = ( int ) Math.ceil ( cacheSize / hashTableLoadFactor ) + 1;
            map = new LinkedHashMap<K, V> ( hashTableCapacity, hashTableLoadFactor, true )
            {
                // (an anonymous inner class)

                private static final long serialVersionUID = 1;

                @Override
                protected boolean removeEldestEntry ( Map.Entry<K, V> eldest )
                {
                    return size () > LRUCache.this.cacheSize;
                }
            };
        }

        /**
         * Retrieves an entry from the cache.<br>
         * The retrieved entry becomes the MRU (most recently used) entry.
         * @param key the key whose associated value is to be returned.
         * @return    the value associated to this key, or null if no value with this key exists in the cache.
         */
        public synchronized V get ( K key )
        {
            return map.get ( key );
        }

        /**
         * Adds an entry to this cache.
         * If the cache is full, the LRU (least recently used) entry is dropped.
         * @param key    the key with which the specified value is to be associated.
         * @param value  a value to be associated with the specified key.
         */
        public synchronized void put ( K key, V value )
        {
            map.put ( key, value );
        }

        /**
         * Clears the cache.
         */
        public synchronized void clear ()
        {
            map.clear ();
        }

        /**
         * Returns the number of used entries in the cache.
         * @return the number of entries currently in the cache.
         */
        public synchronized int usedEntries ()
        {
            return map.size ();
        }

        /**
         * Returns a <code>Collection</code> that contains a copy of all cache entries.
         * @return a <code>Collection</code> with a copy of the cache content.
         */
        public synchronized Collection<Map.Entry<K, V>> getAll ()
        {
            return new ArrayList<Map.Entry<K, V>> ( map.entrySet () );
        }
    } // end class LRUCache

}
